(function(window){
    var urlCount = 0,
        NativeMediaSource = window.MediaSource || window.WebKitMediaSource || {},
        nativeUrl = window.URL || {},
        EventEmitter,
        flvCodec = /video\/flv(;\s*codecs=["']vp6,aac["'])?$/,
        objectUrlPrefix = 'blob:vjs-media-source/';

    EventEmitter = function(){};
    EventEmitter.prototype.init = function(){
        this.listeners = [];
    };
    EventEmitter.prototype.addEventListener = function(type, listener){
        if (!this.listeners[type]){
            this.listeners[type] = [];
        }
        this.listeners[type].unshift(listener);
    };
    EventEmitter.prototype.removeEventListener = function(type, listener){
        var listeners = this.listeners[type],
            i = listeners.length;
        while (i--) {
            if (listeners[i] === listener) {
                return listeners.splice(i, 1);
            }
        }
    };
    EventEmitter.prototype.trigger = function(event){
        var listeners = this.listeners[event.type] || [],
            i = listeners.length;
        while (i--) {
            listeners[i](event);
        }
    };

    // extend the media source APIs

    // Media Source
    videojs.MediaSource = function(){
        var self = this;
        videojs.MediaSource.prototype.init.call(this);

        this.sourceBuffers = [];
        this.readyState = 'closed';
        this.listeners = {
            sourceopen: [function(event){
                // find the swf where we will push media data
                self.swfObj = document.getElementById(event.swfId);
                self.readyState = 'open';

                // trigger load events
                if (self.swfObj) {
                    self.swfObj.vjs_load();
                }
            }],
            webkitsourceopen: [function(event){
                self.trigger({
                    type: 'sourceopen'
                });
            }]
        };
    };
    videojs.MediaSource.prototype = new EventEmitter();

    /**
     * The maximum size in bytes for append operations to the video.js
     * SWF. Calling through to Flash blocks and can be expensive so
     * tuning this parameter may improve playback on slower
     * systems. There are two factors to consider:
     * - Each interaction with the SWF must be quick or you risk dropping
     * video frames. To maintain 60fps for the rest of the page, each append
     * cannot take longer than 16ms. Given the likelihood that the page will
     * be executing more javascript than just playback, you probably want to
     * aim for ~8ms.
     * - Bigger appends significantly increase throughput. The total number of
     * bytes over time delivered to the SWF must exceed the video bitrate or
     * playback will stall.
     *
     * The default is set so that a 4MB/s stream should playback
     * without stuttering.
     */
    videojs.MediaSource.BYTES_PER_SECOND_GOAL = 4 * 1024 * 1024;
    videojs.MediaSource.TICKS_PER_SECOND = 60;

    // create a new source buffer to receive a type of media data
    videojs.MediaSource.prototype.addSourceBuffer = function(type){
        var sourceBuffer;

        // if this is an FLV type, we'll push data to flash
        if (flvCodec.test(type)) {
            // Flash source buffers
            sourceBuffer = new videojs.SourceBuffer(this);
        } else if (this.nativeSource) {
            // native source buffers
            sourceBuffer = this.nativeSource.addSourceBuffer.apply(this.nativeSource, arguments);
        } else {
            throw new Error('NotSupportedError (Video.js)');
        }

        this.sourceBuffers.push(sourceBuffer);
        return sourceBuffer;
    };
    videojs.MediaSource.prototype.endOfStream = function(){
        this.swfObj.vjs_endOfStream();
        this.readyState = 'ended';
    };

    // store references to the media sources so they can be connected
    // to a video element (a swf object)
    videojs.mediaSources = {};
    // provide a method for a swf object to notify JS that a media source is now open
    videojs.MediaSource.open = function(msObjectURL, swfId){
        var mediaSource = videojs.mediaSources[msObjectURL];

        if (mediaSource) {
            mediaSource.trigger({
                type: 'sourceopen',
                swfId: swfId
            });
        } else {
            throw new Error('Media Source not found (Video.js)');
        }
    };

    // Source Buffer
    videojs.SourceBuffer = function(source){
        var self = this,

            // byte arrays queued to be appended
            buffer = [],

            // the total number of queued bytes
            bufferSize = 0,
            scheduleTick = function(func) {
                // Chrome doesn't invoke requestAnimationFrame callbacks
                // in background tabs, so use setTimeout.
                window.setTimeout(func,
                    Math.ceil(1000 / videojs.MediaSource.TICKS_PER_SECOND));
            },
            append = function() {
                var chunk, i, length, payload, maxSize,
                    binary = '';

                if (!buffer.length) {
                    // do nothing if the buffer is empty
                    return;
                }

                if (document.hidden) {
                    // When the document is hidden, the browser will likely
                    // invoke callbacks less frequently than we want. Just
                    // append a whole second's worth of data. It doesn't
                    // matter if the video janks, since the user can't see it.
                    maxSize = videojs.MediaSource.BYTES_PER_SECOND_GOAL;
                } else {
                    maxSize = Math.ceil(videojs.MediaSource.BYTES_PER_SECOND_GOAL/
                        videojs.MediaSource.TICKS_PER_SECOND);
                }

                // concatenate appends up to the max append size
                payload = new Uint8Array(Math.min(maxSize, bufferSize));
                i = payload.byteLength;
                while (i) {
                    chunk = buffer[0].subarray(0, i);

                    payload.set(chunk, payload.byteLength - i);

                    // requeue any bytes that won't make it this round
                    if (chunk.byteLength < buffer[0].byteLength) {
                        buffer[0] = buffer[0].subarray(i);
                    } else {
                        buffer.shift();
                    }

                    i -= chunk.byteLength;
                }
                bufferSize -= payload.byteLength;

                // schedule another append if necessary
                if (bufferSize !== 0) {
                    scheduleTick(append);
                } else {
                    self.trigger({ type: 'updateend' });
                }

                // base64 encode the bytes
                for (i = 0, length = payload.byteLength; i < length; i++) {
                    binary += String.fromCharCode(payload[i]);
                }
                b64str = window.btoa(binary);

                // bypass normal ExternalInterface calls and pass xml directly
                // EI can be slow by default
                self.source.swfObj.CallFunction('<invoke name="vjs_appendBuffer"' +
                    'returntype="javascript"><arguments><string>' +
                    b64str +
                    '</string></arguments></invoke>');
            };

        videojs.SourceBuffer.prototype.init.call(this);
        this.source = source;

        // accept video data and pass to the video (swf) object
        this.appendBuffer = function(uint8Array){
            if (buffer.length === 0) {
                scheduleTick(append);
            }

            this.trigger({ type: 'update' });

            buffer.push(uint8Array);
            bufferSize += uint8Array.byteLength;
        };

        // reset the parser and remove any data queued to be sent to the swf
        this.abort = function() {
            buffer = [];
            bufferSize = 0;
            this.source.swfObj.vjs_abort();
        };
    };
    videojs.SourceBuffer.prototype = new EventEmitter();

    // URL
    videojs.URL = {
        createObjectURL: function(object){
            var url = objectUrlPrefix + urlCount;

            urlCount++;

            // setup the mapping back to object
            videojs.mediaSources[url] = object;

            return url;
        }
    };

    // plugin
    videojs.plugin('mediaSource', function(options){
        var player = this;

        player.on('loadstart', function(){
            var url = player.currentSrc(),
                trigger = function(event){
                    mediaSource.trigger(event);
                },
                mediaSource;

            if (player.techName === 'Html5' && url.indexOf(objectUrlPrefix) === 0) {
                // use the native media source implementation
                mediaSource = videojs.mediaSources[url];

                if (!mediaSource.nativeUrl) {
                    // initialize the native source
                    mediaSource.nativeSource = new NativeMediaSource();
                    mediaSource.nativeSource.addEventListener('sourceopen', trigger, false);
                    mediaSource.nativeSource.addEventListener('webkitsourceopen', trigger, false);
                    mediaSource.nativeUrl = nativeUrl.createObjectURL(mediaSource.nativeSource);
                }
                player.src(mediaSource.nativeUrl);
            }
        });
    });

})(this);
